Skip to content

feat: introduce Authentication abstraction for flexible provider auth#73

Merged
Kamilbenkirane merged 3 commits intomainfrom
feat/authentication-abstraction
Dec 15, 2025
Merged

feat: introduce Authentication abstraction for flexible provider auth#73
Kamilbenkirane merged 3 commits intomainfrom
feat/authentication-abstraction

Conversation

@Kamilbenkirane
Copy link
Copy Markdown
Member

Summary

This PR introduces a flexible authentication abstraction layer that replaces the direct api_key: SecretStr pattern with a pluggable Authentication protocol. This foundational change enables support for diverse authentication methods across providers while maintaining backward compatibility.

Motivation

Different AI providers require different authentication mechanisms:

  • API Keys with varying header formats (Authorization: Bearer, x-api-key, x-goog-api-key, etc.)
  • OAuth tokens (refresh flows, service accounts)
  • Application Default Credentials (Google ADC)
  • Custom authentication (signed requests, HMAC)

The previous approach hardcoded api_key: SecretStr throughout the codebase, making it impossible to support alternative auth methods without invasive changes.

Changes

New: src/celeste/auth.py

  • Authentication — Abstract base class defining the auth protocol with get_headers() -> dict[str, str]
  • APIKey — Concrete implementation supporting configurable header name and prefix
  • register_auth() / get_auth_class() — Registry system for plugin-based auth extensions

Modified: src/celeste/credentials.py

  • Added PROVIDER_AUTH_CONFIG — Maps each provider to its auth configuration (package_name, header, prefix)
  • Added get_auth() method — Returns configured Authentication objects for providers
  • get_credentials() now accepts plain str in addition to SecretStr

Modified: src/celeste/client.py

  • Client.api_key: SecretStrClient.auth: Authentication

Modified: src/celeste/__init__.py

  • create_client() now accepts:
    • api_key: str | SecretStr | None — Improved ergonomics (plain strings auto-convert)
    • auth: Authentication | None — Direct auth object injection for custom auth flows
  • Exports APIKey and Authentication for public use

Modified: tests/unit_tests/test_client.py

  • Updated test fixtures to use auth=APIKey(key=...) pattern

Provider Auth Configuration

Provider Header Prefix
OpenAI Authorization Bearer
Anthropic x-api-key (none)
Google x-goog-api-key (none)
Cohere Authorization bearer
ElevenLabs xi-api-key (none)
BFL x-key (none)
TopazLabs X-API-Key (none)
Others Authorization Bearer

Usage Examples

Before (hardcoded api_key)

client = celeste.create_client(
    capability=Capability.TEXT_GENERATION,
    api_key=SecretStr("sk-xxx"),
)

After (flexible options)

# Simple — plain string (auto-converts to SecretStr)
client = celeste.create_client(
    capability=Capability.TEXT_GENERATION,
    api_key="sk-xxx",
)

# Explicit APIKey with custom header
from celeste import APIKey
client = celeste.create_client(
    capability=Capability.TEXT_GENERATION,
    auth=APIKey(key="sk-xxx", header="X-Custom-Auth"),
)

# Future: Google ADC (when implemented)
from celeste_google import GoogleADC
client = celeste.create_client(
    capability=Capability.TEXT_GENERATION,
    provider=Provider.GOOGLE,
    auth=GoogleADC(),
)

Breaking Changes

  • Client.api_key attribute renamed to Client.auth
  • Type changed from SecretStr to Authentication

Migration: Replace client.api_key with client.auth.key (for APIKey instances)

Test Plan

  • All existing unit tests pass (updated to new pattern)
  • Type checking passes (mypy)
  • Linting passes (ruff)
  • Manual verification with real provider calls

- Add auth.py with Authentication base class and APIKey implementation
- Replace direct SecretStr api_key with Authentication in Client
- Add PROVIDER_AUTH_CONFIG mapping provider-specific headers/prefixes
- Add get_auth() method to Credentials returning Authentication objects
- Allow create_client() to accept plain strings for api_key
- Update tests to use auth=APIKey instead of api_key=SecretStr
@claude
Copy link
Copy Markdown

claude Bot commented Dec 15, 2025

PR Review: Authentication Abstraction Layer

Summary

This PR introduces a well-designed authentication abstraction that replaces the hardcoded api_key: SecretStr pattern with a flexible Authentication protocol. The architecture is solid and the motivation is clear. However, there is a critical bug that will cause runtime failures.


🚨 Critical Issue: Breaking Changes Not Fully Implemented

The Problem

The PR changes Client.api_key to Client.auth (src/celeste/client.py:33), but all provider implementations still reference self.api_key, which will cause AttributeError at runtime.

Affected Files

All provider implementations across all capabilities are broken - 28+ instances found in:

  • Text Generation: openai, anthropic, google, mistral, cohere, xai
  • Image Generation: openai, google, bytedance, bfl
  • Video Generation: openai, google, bytedance
  • Speech Generation: openai, google, elevenlabs

Required Fix

All instances of self.api_key.get_secret_value() must be replaced with using self.auth.get_headers()

Example fix:

# Before (broken):
headers = {
    config.AUTH_HEADER_NAME: f"{config.AUTH_HEADER_PREFIX}{self.api_key.get_secret_value()}",
    "Content-Type": ApplicationMimeType.JSON,
}

# After (correct):
headers = {
    **self.auth.get_headers(),
    "Content-Type": ApplicationMimeType.JSON,
}

✅ Positive Aspects

Architecture & Design

  1. Excellent abstraction design - The Authentication protocol with get_headers() is clean and extensible
  2. Proper use of ABC - Abstract base class pattern is appropriate
  3. Backward compatibility - The api_key parameter still works, gets converted to APIKey auth
  4. Registry pattern - Enables plugin-based auth extensions
  5. Clear separation of concerns - Auth logic isolated from client logic

Implementation Quality

  1. Type safety - Good use of str | SecretStr with automatic conversion via field validator
  2. Configuration mapping - PROVIDER_AUTH_CONFIG centralizes provider-specific auth settings
  3. Ergonomics - Plain string support improves developer experience
  4. Documentation - Comprehensive docstrings and PR description

🔍 Code Quality Observations

src/celeste/auth.py

Good: Clean module, elegant field validator pattern, proper exports

Minor: Line 69 imports inside function - consider caching loaded state

src/celeste/credentials.py

Good: Consistent ordering, proper error handling, clear precedence

Suggestion: Line 162 - _package_name is unused, consider documenting or removing


🧪 Testing

Coverage: Unit tests updated correctly but integration tests will fail due to the critical bug

Missing tests to consider:

  1. Custom auth objects
  2. Header generation per provider
  3. Error cases when neither api_key nor auth provided
  4. Priority testing for auth vs api_key parameters

🔒 Security Considerations

Good practices:

  • SecretStr usage prevents accidental logging
  • Field(exclude=True) prevents serialization
  • Environment variable isolation

Consider: Adding __repr__ override to APIKey to prevent accidental exposure


⚡ Performance

No regression - negligible overhead, connection pooling preserved, lazy loading maintained


🎯 Recommendations

Before Merging (Required)

  1. Fix all provider implementations - Update all 28+ instances of self.api_key
  2. Run full test suite - Including integration tests
  3. Update changelog - Document breaking changes

Nice to Have

  1. Add custom Authentication implementation example to docs
  2. Consider temporary compatibility layer with @property for api_key
  3. Add integration test creating client with each provider

📊 Overall Assessment

Category Rating Notes
Architecture ⭐⭐⭐⭐⭐ Excellent design
Implementation ⭐⭐ Critical bug blocks merge
Testing ⭐⭐⭐ Unit tests good, integration needed
Documentation ⭐⭐⭐⭐ Clear and comprehensive
Security ⭐⭐⭐⭐ Proper credential handling

Verdict: Strong architectural foundation, but cannot be merged until all provider implementations are updated. Once fixed, this will be an excellent addition to the codebase.

- Replace self.api_key.get_secret_value() with self.auth.get_headers()
- Update all text-generation provider clients (OpenAI, Anthropic, Google, Cohere, Mistral, XAI)
- Update all image-generation provider clients (OpenAI, Google, BFL, ByteDance)
- Update all speech-generation provider clients (OpenAI, Google, ElevenLabs)
- Update all video-generation provider clients (OpenAI, Google, ByteDance)
- Update test fixtures to use auth=APIKey() instead of api_key=SecretStr()
- Add packages/providers/* to uv workspace members
- Add ANN401 ignore for client.py files in ruff config
- Remove noqa comments from src/celeste/client.py (now handled by config)
- Update Makefile typecheck to use packages/*/*/src/ glob
- Update Makefile test to run only core unit tests
- Add capability argument support to make integration-test
@claude
Copy link
Copy Markdown

claude Bot commented Dec 15, 2025

Pull Request Review - PR #73

Thank you for this well-structured PR introducing the Authentication abstraction! This is a thoughtful architectural improvement that enables flexible authentication methods. Below is my detailed review:


Strengths

Architecture & Design

  • Excellent abstraction design: The Authentication protocol with get_headers() is clean and extensible
  • Future-proof: Registry system (register_auth, get_auth_class) enables plugin-based extensions
  • Backward compatibility: The dual api_key and auth parameters in create_client() provide a smooth migration path
  • Consistent application: Authentication changes are systematically applied across all 17+ provider clients

Code Quality

  • Type safety: Proper use of SecretStr, field validators, and type hints throughout
  • DRY principle: Centralized PROVIDER_AUTH_CONFIG eliminates repetitive header/prefix definitions
  • Clean separation: Auth logic cleanly separated from credential management
  • Documentation: Excellent PR description with motivation, changes, and migration guide

🔴 Critical Issues

1. Missing Test Coverage for New Auth Module ⚠️

Location: src/celeste/auth.py

Issue: The new auth.py module (80 lines) has zero test coverage. Critical functionality is untested:

  • APIKey.get_headers() method
  • APIKey.convert_to_secret() validator (string → SecretStr conversion)
  • register_auth() function
  • get_auth_class() function
  • Edge cases (empty prefix, missing header, etc.)

Impact: HIGH - Core authentication logic could have bugs that won't be caught

Recommendation: Add comprehensive tests:

# tests/unit_tests/test_auth.py
def test_api_key_get_headers_with_prefix():
    auth = APIKey(key="sk-123", header="Authorization", prefix="Bearer ")
    assert auth.get_headers() == {"Authorization": "Bearer sk-123"}

def test_api_key_get_headers_without_prefix():
    auth = APIKey(key="key-123", header="x-api-key", prefix="")
    assert auth.get_headers() == {"x-api-key": "key-123"}

def test_api_key_accepts_plain_string():
    auth = APIKey(key="plain-string")  # Should auto-convert to SecretStr
    assert isinstance(auth.key, SecretStr)

def test_register_and_retrieve_auth_class():
    # Test registry functionality

2. Incomplete Test Updates for get_auth() Method ⚠️

Location: tests/unit_tests/test_credentials.py

Issue: The Credentials.get_auth() method (38 lines, credentials.py:132-171) has no test coverage. The PR only updates existing tests to use APIKey but doesn't test the new authentication retrieval logic.

Missing test scenarios:

  • override_auth parameter takes precedence
  • override_key as plain string converts properly
  • Correct APIKey instance returned with provider-specific headers/prefixes
  • Error handling when provider not in PROVIDER_AUTH_CONFIG

Recommendation: Add test class:

class TestGetAuth:
    def test_get_auth_returns_api_key_with_correct_config():
        # Verify Anthropic returns x-api-key header
    
    def test_get_auth_override_auth_takes_precedence():
        # Custom auth object should be used
    
    def test_get_auth_with_plain_string_override():
        # Plain string should convert to SecretStr

3. Circular Import Risk ⚠️

Location: src/celeste/auth.py:69, src/celeste/credentials.py:7

Issue: Potential circular import between auth.py and credentials.py:

  • credentials.py imports from celeste.auth import APIKey, Authentication (line 7)
  • auth.get_auth_class() imports from celeste.registry import _load_from_entry_points (line 69)
  • If registry.py imports from credentials.py, this creates a cycle

Current state: Not broken yet, but fragile. The import in get_auth_class() is at function level (good), but still risky.

Recommendation:

  1. Keep the lazy import in get_auth_class() (already done ✓)
  2. Add a comment explaining the import is intentionally lazy to avoid cycles
  3. Consider moving _auth_classes registry to registry.py for consistency

⚠️ Medium Priority Issues

4. Inconsistent Error Handling in get_auth()

Location: src/celeste/credentials.py:157-159

Issue: When a provider is not in PROVIDER_AUTH_CONFIG, it raises UnsupportedProviderError, but this differs from the error raised in get_credentials() which raises MissingCredentialsError. The distinction isn't clear.

auth_config = PROVIDER_AUTH_CONFIG.get(provider)
if not auth_config:
    raise UnsupportedProviderError(provider=provider)  # Line 159

Recommendation: Document why these are different errors, or consider if get_auth() should raise MissingCredentialsError when credentials are missing (line 165) vs UnsupportedProviderError when the provider isn't configured.


5. Unused package_name in Auth Config

Location: src/celeste/credentials.py:13, 162

Issue: The PROVIDER_AUTH_CONFIG includes package_name as the first tuple element, but it's immediately discarded:

PROVIDER_AUTH_CONFIG: dict[Provider, tuple[str, str, str]] = {
    Provider.OPENAI: ("celeste_openai", "Authorization", "Bearer "),
    #                 ^^^^^^^^^^^^^^^ Never used
}

_package_name, header, prefix = auth_config  # Line 162 - discarded

Recommendation:

  • If package_name is for future use, add a comment explaining its purpose
  • If it's not needed, remove it to simplify the config: tuple[str, str] instead of tuple[str, str, str]

6. Security: Plain String API Keys in Public API

Location: src/celeste/__init__.py:65, src/celeste/credentials.py:78

Issue: The PR allows plain str API keys for convenience, but this could encourage insecure practices:

def create_client(
    api_key: str | SecretStr | None = None,  # Plain strings allowed

Security consideration: While convenient, plain strings can accidentally appear in logs, tracebacks, or debugging output. The validator converts them to SecretStr, but only after they've been passed around as plain strings.

Recommendation:

  • This is acceptable for developer ergonomics, but add a docstring warning:
    api_key: Optional API key override. For security, prefer SecretStr over plain strings
              in production code to prevent accidental exposure in logs.

💡 Minor Issues & Suggestions

7. Inconsistent Prefix Casing

Location: src/celeste/credentials.py:21

Issue: Cohere uses lowercase "bearer " while others use capitalized "Bearer ":

Provider.COHERE: ("celeste_cohere", "Authorization", "bearer "),  # lowercase
Provider.OPENAI: ("celeste_openai", "Authorization", "Bearer "),  # capitalized

Question: Is this intentional (Cohere requires lowercase)? If so, add a comment. If not, standardize.


8. Missing Docstring for APIKey Methods

Location: src/celeste/auth.py:42-44

Issue: The get_headers() implementation lacks a docstring, while the abstract method has one.

Recommendation: Add brief docstring or rely on inheritance from abstract method.


9. Removed noqa: ANN401 Comment

Location: packages/capabilities/image-generation/src/celeste_image_generation/providers/google/client.py:31

Issue: The diff shows removal of # noqa: ANN401 from model_post_init:

-    def model_post_init(self, __context: Any) -> None:  # noqa: ANN401
+    def model_post_init(self, __context: Any) -> None:

Question: Why was this removed? If the linter no longer complains, great! But if it's an oversight, the comment should be restored.


10. BFL Provider Reordering

Location: src/celeste/credentials.py:47, 68

Issue: BFL's credential field was moved from line 34 to line 68 in PROVIDER_CREDENTIAL_MAP, and the attribute was moved to the end of the class. This is cosmetic but inconsistent with alphabetical ordering.

Recommendation: Keep consistent ordering (alphabetical) for maintainability.


📊 Test Coverage Assessment

Files Changed vs Files Tested

  • 22 files modified in this PR
  • 1 test file updated (test_client.py)
  • 0 new test files added

Coverage Gaps

File Lines Added Test Coverage
src/celeste/auth.py 80 ❌ 0%
src/celeste/credentials.py (new methods) 38 ❌ 0%
Provider clients (17 files) ~34 ✅ Indirectly tested

Overall: Test coverage for new code is insufficient for a production merge.


🔒 Security Review

✅ Secure Practices

  • Proper use of SecretStr throughout
  • Authentication headers correctly constructed
  • No credential leakage in error messages

⚠️ Considerations


🚀 Performance Considerations

No performance concerns identified:

  • Header generation is a simple dictionary operation
  • Lazy imports used appropriately
  • No additional HTTP requests or blocking operations

📋 Recommendations Summary

Must-Fix Before Merge

  1. ✅ Add comprehensive tests for src/celeste/auth.py
  2. ✅ Add tests for Credentials.get_auth() method
  3. ✅ Document or remove unused package_name in PROVIDER_AUTH_CONFIG

Should Fix

  1. Add docstring warning about plain string security implications
  2. Clarify error handling strategy (UnsupportedProviderError vs MissingCredentialsError)
  3. Add comment explaining lazy import in get_auth_class() (circular dependency prevention)

Nice to Have

  1. Verify Cohere's lowercase "bearer " is intentional (add comment if so)
  2. Restore # noqa: ANN401 if needed
  3. Standardize provider ordering in credential fields

📝 Migration Notes for Users

The PR description excellently covers migration. One suggestion:

Add to README/CHANGELOG:

# ⚠️ BREAKING CHANGE in v0.3.0
# client.api_key is now client.auth

# Before
api_key_value = client.api_key.get_secret_value()

# After (for APIKey auth)
api_key_value = client.auth.key.get_secret_value()

Conclusion

This is a well-architected PR that significantly improves the flexibility and extensibility of the authentication system. The design is sound, the implementation is clean, and the changes are comprehensive.

Primary blocker: Insufficient test coverage for critical new functionality.

Recommendation: Add tests for auth.py and get_auth(), address the unused package_name issue, then this PR will be ready to merge.

Great work on this foundational improvement! 🎉


Reviewed with 🤖 Claude Code

@Kamilbenkirane Kamilbenkirane merged commit 02efd01 into main Dec 15, 2025
11 checks passed
@claude
Copy link
Copy Markdown

claude Bot commented Dec 15, 2025

Pull Request Review: Authentication Abstraction Layer

Summary

This PR introduces a well-architected authentication abstraction that successfully decouples authentication mechanisms from the provider clients. The implementation is clean, follows good design patterns, and maintains backward compatibility while enabling future extensibility.

✅ Strengths

Architecture & Design

  • Excellent abstraction: The Authentication protocol with get_headers() is simple and extensible
  • Clean separation of concerns: Auth logic is properly decoupled from client implementation
  • Registry pattern: Consistent with existing _clients and _models registries
  • Future-proof: Enables OAuth, ADC, and custom auth without breaking changes

Code Quality

  • Type safety: Proper use of SecretStr and type annotations throughout
  • Consistent patterns: All provider clients updated uniformly with **self.auth.get_headers()
  • Good documentation: Comprehensive docstrings and inline comments
  • Test coverage: Updated existing tests to use new APIKey pattern

User Experience

  • Backward compatibility maintained: Both api_key (str/SecretStr) and auth parameters supported
  • Improved ergonomics: Plain strings now auto-convert to SecretStr (src/celeste/auth.py:34-40)
  • Clear migration path: Breaking changes documented with migration guidance

🔍 Issues & Recommendations

1. Missing Test Coverage for New Auth Module ⚠️ HIGH PRIORITY

Location: src/celeste/auth.py

Issue: The new auth.py module (80 lines) has no dedicated unit tests. Only indirect testing through updated client tests.

Missing coverage:

  • APIKey.convert_to_secret() validator (lines 34-40)
  • APIKey.get_headers() with various header/prefix combinations
  • register_auth() and get_auth_class() registry functions
  • Error handling in get_auth_class() when auth type not found

Recommendation: Create tests/unit_tests/test_auth.py with comprehensive test coverage for the new auth module.

2. Missing Tests for Credentials.get_auth() Method ⚠️ HIGH PRIORITY

Location: src/celeste/credentials.py:132-171

Issue: The new get_auth() method is core functionality but not tested in tests/unit_tests/test_credentials.py.

Missing coverage:

  • override_auth takes precedence
  • Correct APIKey construction from PROVIDER_AUTH_CONFIG
  • UnsupportedProviderError for unmapped providers
  • Interaction between override_auth and override_key parameters

Recommendation: Add TestGetAuth class to test_credentials.py with full coverage.

3. Inconsistent Ruff Configuration Change

Location: pyproject.toml:141

Issue: Added blanket exception for ANN401 (Allow Any) for all client.py files. This disables the "Any" type check for all code in any client.py file, not just return types or mixins.

Recommendation:

  • Remove the per-file ignore from pyproject.toml
  • Keep inline noqa for specific *args: Any parameters where type variance is intentional
  • This maintains stricter type checking without unnecessary blanket exceptions

4. Unused package_name in Auth Config

Location: src/celeste/credentials.py:13-29, 162

Issue: PROVIDER_AUTH_CONFIG maps to 3-tuples (package_name, header, prefix), but package_name is assigned to _package_name and never used.

Recommendation:

  • If keeping for future use: Add a comment explaining the intent
  • If not needed: Simplify to 2-tuple (header, prefix) to reduce confusion

5. Makefile Test Changes Scope Creep

Location: Makefile:40-52

Issue: The Makefile changes (integration test filtering, typecheck glob improvements) are good improvements but unrelated to auth abstraction.

Recommendation: Consider splitting these into a separate PR for cleaner history.


🔒 Security Review

✅ Secure Practices

  • Proper use of SecretStr throughout
  • APIKey.get_headers() only exposes secrets when constructing headers (unavoidable)
  • No secrets in logs or repr/str methods
  • Existing security tests pass (test_credentials.py::TestCredentialSecurity)

⚠️ Consideration

  • The get_headers() method returns plain dict[str, str] with unmasked keys - this is necessary for HTTP requests but means downstream code must handle these dicts carefully
  • Recommend adding a docstring warning to Authentication.get_headers() about not logging/caching the return value

📋 Testing

What's Tested ✅

  • Client validation with auth parameter
  • Provider client header construction via **self.auth.get_headers()
  • Credentials loading from environment
  • Override key functionality

What's NOT Tested ❌

  • auth.py module (0% coverage)
  • Credentials.get_auth() method
  • APIKey field validator
  • Auth registry functions
  • Provider auth config correctness

Impact: Core new functionality is untested, risking regressions.


🎯 Action Items

Before Merge (Blockers)

  1. Add unit tests for src/celeste/auth.py (new file: tests/unit_tests/test_auth.py)
  2. Add tests for Credentials.get_auth() (extend tests/unit_tests/test_credentials.py)
  3. Remove or clarify unused package_name in auth config
  4. Remove blanket ANN401 exception from pyproject.toml (keep inline noqa instead)

Recommended (Non-blocking)

  1. Add security warning to Authentication.get_headers() docstring
  2. Add comment explaining type conversion for api_key parameter
  3. Verify PROVIDER_AUTH_CONFIG correctness with actual provider API docs
  4. Consider splitting Makefile changes into separate PR

📊 Risk Assessment

Area Risk Rationale
Breaking Changes Low Well-documented, only affects Client.api_keyClient.auth
Regression Risk Medium Missing test coverage for new code paths
Security Low Proper SecretStr usage, no new exposure vectors
Performance None No performance impact
Maintainability High (Positive) Significantly improves extensibility

🎉 Conclusion

This is a well-designed and thoughtfully implemented feature that significantly improves Celeste's architecture. The abstraction is clean, the migration path is clear, and the code quality is high.

Main concern: Test coverage gaps for core new functionality (auth.py and get_auth()). These should be addressed before merge to prevent regressions and ensure the new abstraction works correctly across all edge cases.

Once test coverage is added, this PR will be ready to merge. Great work! 🚀

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant